package tcards.android;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.UUID;

import models.Command;

import org.codehaus.jackson.map.ObjectMapper;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

public class BluetoothService {


	/**
	 * This class does all the work for setting up and managing Bluetooth
	 * connections with other devices. It has a thread that listens for
	 * incoming connections, a thread for connecting with a device, and a
	 * thread for performing data transmissions when connected.
	 */
	    // Debugging
	    private static final String TAG = "BluetoothChatService";
	    private static final boolean D = true;

	    // Unique UUID for this application
	    private static final UUID MY_UUID = UUID.fromString("04c6093b-0000-1000-8000-00805f9b34fb");

	    // Member fields
	    private final BluetoothAdapter mAdapter;
	    private final Handler mHandler;
	    private ConnectThread mConnectThread;
	    private ConnectedThread mConnectedThread;
	    private int mState;
	    private final ObjectMapper mMapper;

	    // Constants that indicate the current connection state
	    public static final int STATE_NONE = 0;       // we're doing nothing
	    public static final int STATE_CONNECTING = 1; // now initiating an outgoing connection
	    public static final int STATE_CONNECTED = 2;  // now connected to a remote device

	    /**
	     * Constructor. Prepares a new BluetoothChat session.
	     * @param context  The UI Activity Context
	     * @param handler  A Handler to send messages back to the UI Activity
	     */
	    public BluetoothService(Context context, Handler handler) {
	        mAdapter = BluetoothAdapter.getDefaultAdapter();
	        mState = STATE_NONE;
	        mHandler = handler;
	        mMapper = new ObjectMapper();
	        //mMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, As.PROPERTY);
	    }

	    /**
	     * Set the current state of the chat connection
	     * @param state  An integer defining the current connection state
	     */
	    private synchronized void setState(int state) {
	        if (D) Log.d(TAG, "setState() " + mState + " -> " + state);
	        mState = state;

	        // Give the new state to the Handler so the UI Activity can update
	        //mHandler.obtainMessage(TcardsAndroidActivity.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
	    }

	    /**
	     * Return the current connection state. */
	    public synchronized int getState() {
	        return mState;
	    }

	    /**
	     * Start the ConnectThread to initiate a connection to a remote device.
	     * @param device  The BluetoothDevice to connect
	     * @param secure Socket Security type - Secure (true) , Insecure (false)
	     */
	    public synchronized void connect(BluetoothDevice device) {
	        if (D) Log.d(TAG, "connect to: " + device);

	        // Cancel any thread attempting to make a connection
	        if (mState == STATE_CONNECTING) {
	            if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
	        }

	        // Cancel any thread currently running a connection
	        if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

	        // Start the thread to connect with the given device
	        mConnectThread = new ConnectThread(device);
	        mConnectThread.start();
	        setState(STATE_CONNECTING);
	    }

	    /**
	     * Start the ConnectedThread to begin managing a Bluetooth connection
	     * @param socket  The BluetoothSocket on which the connection was made
	     * @param device  The BluetoothDevice that has been connected
	     */
	    public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
	        if (D) Log.d(TAG, "connected");

	        // Cancel the thread that completed the connection
	        if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}

	        // Cancel any thread currently running a connection
	        if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

	        // Start the thread to manage the connection and perform transmissions
	        mConnectedThread = new ConnectedThread(socket);
	        mConnectedThread.start();

	        // Send the name of the connected device back to the UI Activity        
	        Message msg = mHandler.obtainMessage(TcardsAndroidActivity.TOAST);
	        msg.obj =  "Successfully connected to " + device.getName();
	        msg.sendToTarget();

	        setState(STATE_CONNECTED);
	    }

	    /**
	     * Stop all threads
	     */
	    public synchronized void stop() {
	        if (D) Log.d(TAG, "stop");

	        if (mConnectThread != null) {
	            mConnectThread.cancel();
	            mConnectThread = null;
	        }

	        if (mConnectedThread != null) {
	            mConnectedThread.cancel();
	            mConnectedThread = null;
	        }

	        setState(STATE_NONE);
	    }

	    /**
	     * Write to the ConnectedThread in an unsynchronized manner
	     * @param out The bytes to write
	     * @throws Exception 
	     * @see ConnectedThread#write(byte[])
	     */
	    public void sendCommand(Command cmd) throws Exception {
	        // Create temporary object
	        ConnectedThread r;
	        // Synchronize a copy of the ConnectedThread
	        synchronized (this) {
	            if (mState != STATE_CONNECTED) 
	            	return;
	            r = mConnectedThread;
	        }
	        
	        // Perform the write unsynchronized
	        r.write(writeCommand(cmd));
	    }

	    /**
	     * Indicate that the connection attempt failed and notify the UI Activity.
	     */
	    private void connectionFailed() {
	        // Send a failure message back to the Activity
	        Message msg = mHandler.obtainMessage(TcardsAndroidActivity.TOAST);
	        msg.obj =  "Unable to connect device";
	        msg.sendToTarget();
	    }

	    /**
	     * Indicate that the connection was lost and notify the UI Activity.
	     */
	    private void connectionLost() {
	        // Send a failure message back to the Activity
	        Message msg = mHandler.obtainMessage(TcardsAndroidActivity.TOAST);
	        msg.obj =  "Device connection was lost";
	        msg.sendToTarget();
	    }
	    
		private Command readCommand(byte[] bytes) throws IOException
		{
			Command command = null;
			
			try	{	
				command = mMapper.readValue(bytes, Command.class);
			} catch (Exception e) {
				Log.e(TAG, "Failed to process incomming Command:\n", e);
			}
			
			return command;			
		}
		
		private byte[] writeCommand(Command cmd) throws Exception
		{	
			try {					
				return mMapper.writeValueAsBytes(cmd);
			} catch (Exception e) {
				Log.e(TAG, "Failed to serialize Tcard into byte array:\n", e);
				throw e;
			}
		}

	    /**
	     * This thread runs while attempting to make an outgoing connection
	     * with a device. It runs straight through; the connection either
	     * succeeds or fails.
	     */
	    private class ConnectThread extends Thread {
	        private final BluetoothSocket mmSocket;
	        private final BluetoothDevice mmDevice;

	        public ConnectThread(BluetoothDevice device) {
	            mmDevice = device;
	            BluetoothSocket tmp = null;

	            // Get a BluetoothSocket for a connection with the
	            // given BluetoothDevice
	            try {
	    	        tmp = device.createRfcommSocketToServiceRecord(MY_UUID);        	
	            } catch (Exception e) {
	                Log.e(TAG, "Socket create() failed", e);
	            }
	            mmSocket = tmp;
	        }

	        public void run() {
	            Log.i(TAG, "BEGIN mConnectThread");
	            setName("ConnectThread");

	            // Always cancel discovery because it will slow down a connection
	            mAdapter.cancelDiscovery();

	            // Make a connection to the BluetoothSocket
	            try {
	                // This is a blocking call and will only return on a
	                // successful connection or an exception
	                mmSocket.connect();
	            } catch (IOException e) {
	            	Log.e(TAG, "Failed to connect: " + e.toString(), e);
	                
	                try {
	                    mmSocket.close();
	                } catch (IOException e2) {
	                    Log.e(TAG, "unable to close() socket during connection failure", e2);
	                }
	                connectionFailed();
	                return;
	            }

	            // Reset the ConnectThread because we're done
	            synchronized (BluetoothService.this) {
	                mConnectThread = null;
	            }

	            // Start the connected thread
	            connected(mmSocket, mmDevice);
	        }

	        public void cancel() {
	            try {
	                mmSocket.close();
	            } catch (IOException e) {
	                Log.e(TAG, "close() of connect socket failed", e);
	            }
	        }
	    }

	    /**
	     * This thread runs during a connection with a remote device.
	     * It handles all incoming and outgoing transmissions.
	     */
	    private class ConnectedThread extends Thread {
	        private final BluetoothSocket mmSocket;
	        private final DataInputStream mmInStream;
	        private final DataOutputStream mmOutStream;
	        private boolean mIsCanceled;

	        public ConnectedThread(BluetoothSocket socket) {
	            Log.d(TAG, "create ConnectedThread");
	            mmSocket = socket;
	            DataInputStream tmpIn = null;
	            DataOutputStream tmpOut = null;
	            mIsCanceled = false;

	            // Get the BluetoothSocket input and output streams
	            try {
	                tmpIn = new DataInputStream(socket.getInputStream());
	                tmpOut = new DataOutputStream(socket.getOutputStream());
	            } catch (IOException e) {
	                Log.e(TAG, "temp sockets not created", e);
	            }

	            mmInStream = tmpIn;
	            mmOutStream = tmpOut;
	        }

	        public void run() {
	            Log.i(TAG, "BEGIN mConnectedThread");

		        try {            
		            while (true) {
	                    int length = 0;
	                    
	                    length = mmInStream.readInt();
	                    byte[] buffer = new byte[length];
	                    mmInStream.readFully(buffer);
	                    
	                    if(D) Log.d(TAG, "Got Bytes: " + length + "\n" + new String(buffer));                    
	                    
	                    Command cmd = readCommand(buffer); 

	                    // Send the obtained bytes to the UI Activity
	                    Message msg = mHandler.obtainMessage(TcardsAndroidActivity.COMMAND);
	                    msg.obj = cmd;	       
	        	        msg.sendToTarget();
		            }
                } catch (IOException e) {
                    Log.e(TAG, "disconnected", e);
                    if(!mIsCanceled)
                    	connectionLost();
                }
	        }

	        /**
	         * Write to the connected OutStream.
	         * @param buffer  The bytes to write
	         */
	        public void write(byte[] buffer) {
	            try {
	            	mmOutStream.writeInt(buffer.length);
	                mmOutStream.write(buffer);
	            } catch (IOException e) {
	                Log.e(TAG, "Exception during write", e);
	            }
	        }

	        public void cancel() {
	            try {
	            	mIsCanceled = true;
	                mmSocket.close();
	            } catch (IOException e) {
	                Log.e(TAG, "close() of connect socket failed", e);
	            }
	        }
	    }
	    
	    


}
